﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using LibNoise.Xna;
using LibNoise.Xna.Operator;
using LibNoise.Xna.Generator;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace StrategyGame
{
    internal struct GeneratedTile
    {
        public double Altitude;
        public double Temperature;
        public double Forestation;
    }

	internal interface IWorldGenerator
	{
		/// <summary>
		/// Returns an array of info structures representing a rectangular area of map generated by this generator function
		/// </summary>
		/// <param name="width">Width of the area to generate</param>
		/// <param name="height">Height of the area to generate</param>
		/// <param name="seed">Seed to use for randomization</param>
		/// <param name="device">Graphics device to use (required since LibNoise.Xna uses textures for output)</param>
		/// <returns>An array of GeneratedTile structures with a length of (width*height)</returns>
		GeneratedTile[] GenerateWorld(int width, int height, int seed, GraphicsDevice device);
	}

	/// <summary>
	/// Generator that attempts to create two main landmasses separated by ocean
	/// </summary>
	class TwoContinentsWorldGenerator: IWorldGenerator
	{
		#region Implementation of IWorldGenerator

		public GeneratedTile[] GenerateWorld(int width, int height, int seed, GraphicsDevice device)
		{
			ModuleBase altitude_perlin = new Perlin(0.02, 1.5, 10, 2, seed, LibNoise.Xna.QualityMode.High);
			altitude_perlin = new TransformToPositiveRange(altitude_perlin);

			ModuleBase oceans = new Perlin(0.01, 4, 0.5, 2, seed, LibNoise.Xna.QualityMode.High);
			oceans = new TransformToPositiveRange(oceans);

			ModuleBase rigged = new RiggedMultifractal((1 / (float)width) * 2, 1.5, 3, seed, LibNoise.Xna.QualityMode.High);
			rigged = new TransformToPositiveRange(rigged);

			//Invert inverted_rigged = new Invert(perlin);
			ModuleBase minus_oceans = new Multiply(altitude_perlin, rigged);

			LinearRamp vertical_divide_ramp = new LinearRamp(width, LinearRamp.Direction.X);
			vertical_divide_ramp.SetPin(0.00, 1);
			vertical_divide_ramp.SetPin(0.25, 0.0);
			vertical_divide_ramp.SetPin(0.50, 1);
			vertical_divide_ramp.SetPin(0.75, 0.0);
			vertical_divide_ramp.SetPin(1.00, 1);
			ModuleBase verticaldivide_noise = new Perlin(0.02, 4, 0.5, 2, seed + 1, LibNoise.Xna.QualityMode.Medium);
			verticaldivide_noise = new TransformToPositiveRange(verticaldivide_noise);

			ModuleBase verticaldivide = new Clamp(0, 1, new Add(verticaldivide_noise, vertical_divide_ramp));

			ModuleBase create_central_ocean = new Multiply(minus_oceans, new OneMinus(verticaldivide));

			var landmasses = new UnTransformToPositiveRange(create_central_ocean);

			Noise2D landmasses_renderer = new LibNoise.Xna.Noise2D(width, height, landmasses);//final_output);
			landmasses_renderer.GeneratePlanar(0, width, 0, height, true);

			Texture2D land_texture = landmasses_renderer.GetTexture(device, Gradient.Grayscale);
			Color[] land_data = new Color[width * height];
			land_texture.GetData(land_data);
			//land_texture.Dispose();

			//temperature generation
			ModuleBase temperature_noise = new Perlin(height / 1 * 0.0005, 10, 1, 2, seed + 1, LibNoise.Xna.QualityMode.Medium);
			temperature_noise = new TransformToPositiveRange(temperature_noise);

			Const temperate = new Const(0.5);

			LinearRamp poles_ramp = new LinearRamp(width, LinearRamp.Direction.Z);
			poles_ramp.SetPin(0.0, 1);
			poles_ramp.SetPin(0.15, 0);
			poles_ramp.SetPin(0.25, 0);
			poles_ramp.SetPin(0.75, 0);
			poles_ramp.SetPin(0.85, 0);
			poles_ramp.SetPin(1.0, 1);

			ModuleBase poles_temperature = new Clamp(0, 1, new Subtract(poles_ramp, temperature_noise));
			poles_temperature = new OneMinus(new Clamp(0, 1, poles_temperature));
			poles_temperature = new Subtract(poles_temperature, new Multiply(new Const(1), poles_ramp));

			ModuleBase temperature_module = new Multiply(temperate, poles_temperature);

			LinearRamp equator_ramp = new LinearRamp(width, LinearRamp.Direction.Z);
			equator_ramp.SetPin(0.00, 0);
			equator_ramp.SetPin(0.25, 0);
			equator_ramp.SetPin(0.50, 1);
			equator_ramp.SetPin(0.75, 0);
			equator_ramp.SetPin(1.00, 0);

			ModuleBase equator_temperature = new Multiply(temperature_noise, equator_ramp);
			//ModuleBase equator_basetemp = new Const(1.0);

			temperature_module = new Clamp(0, 1, new Add(equator_temperature, temperature_module));

			temperature_module = new UnTransformToPositiveRange(temperature_module);

			Noise2D temperature_renderer = new Noise2D(width, height, temperature_module);
			temperature_renderer.GeneratePlanar(0, width, 0, height, true);

			Texture2D temperature_texture = temperature_renderer.GetTexture(device, Gradient.Grayscale);
			Color[] temperature_data = new Color[width * height];
			temperature_texture.GetData(temperature_data);
			//temperature_texture.Dispose();

			ModuleBase forest_noise = new Perlin((1 / (double)width) * 20, 4, 2, 2, seed + 2, QualityMode.Medium);

			Noise2D forest_renderer = new Noise2D(width, height, forest_noise);
			forest_renderer.GeneratePlanar(0, width, 0, height, true);
			Texture2D forest_texture = forest_renderer.GetTexture(device);
			Color[] forest_data = new Color[width * height];
			forest_texture.GetData(forest_data);

			GeneratedTile[] result = new GeneratedTile[width * height];

			int tile_counter = 0;
			for (int x = 0; x < width; ++x)
			{
				for (int y = 0; y < height; ++y)
				{
					result[tile_counter].Altitude = (double)land_data[tile_counter].R / 255.0;
					result[tile_counter].Temperature = (double)temperature_data[tile_counter].R / 255.0;
					result[tile_counter].Forestation = (double)forest_data[tile_counter].R / 255.0;

					++tile_counter;
				}
			}

			return result;
		}
		#endregion
	}

	#region LibNoise node-based workflow modules

	/// <summary>
	/// module to map -1..1 outputs to 0..1 outputs
	/// </summary>
	class TransformToPositiveRange : ModuleBase
	{
		public TransformToPositiveRange(ModuleBase input)
			: base(1)
		{
			this.m_modules[0] = input;
		}

		public override double GetValue(double x, double y, double z)
		{
			double inputval = m_modules[0].GetValue(x, y, z);

			double clamped = Math.Max(-1, Math.Min(1, inputval));

			return (clamped + 1) * 0.5;
		}
	}

	/// <summary>
	/// module to map 0..1 outputs to -1..1 outputs
	/// </summary>
	class UnTransformToPositiveRange : ModuleBase
	{
		public UnTransformToPositiveRange(ModuleBase input)
			: base(1)
		{
			this.m_modules[0] = input;
		}

		public override double GetValue(double x, double y, double z)
		{
			double inputval = m_modules[0].GetValue(x, y, z);

			double clamped = Math.Max(0, Math.Min(1, inputval));

			return (clamped * 2) - 1.0;
		}
	}

	/// <summary>
	/// module to produce a linear ramp of values defined by 2 or more pins
	/// </summary>
	class LinearRamp : ModuleBase
	{
		#region fields
		public enum Direction
		{
			X, Y, Z
		}

		Direction GradientDirection = Direction.X;

		double Size;

		#endregion

		#region methods
		SortedList<double, double> Pins = new SortedList<double, double>();

		public void SetPin(double pos, double value)
		{
			Pins[pos] = value;
		}

		public override double GetValue(double x, double y, double z)
		{
			double var;
			switch (GradientDirection)
			{
				case Direction.X:
					var = x;
					break;
				case Direction.Y:
					var = y;
					break;
				case Direction.Z:
					var = z;
					break;
				default:
					var = 0.0;
					break;
			}

			var /= Size;

			if (var > 1.0 || var < 0.0)
				var = Math.Abs(var) - Math.Truncate(var);

			KeyValuePair<double, double> start_pin = new KeyValuePair<double, double>(0, -1);

			foreach (var pin in Pins)
			{
				if (pin.Key < var)
					start_pin = pin;
			}

			int start_idx = Pins.IndexOfKey(start_pin.Key);

			double end_key = Pins.Keys[start_idx + 1];
			double end_value = Pins[end_key];

			var pos_between_pins = (var - start_pin.Key) / (end_key - start_pin.Key);

			//KeyValuePair<double, double> end_pin = new KeyValuePair<double, double>(end_key, end_value);

			//lerp
			double result = start_pin.Value + ((end_value - start_pin.Value) * pos_between_pins);

			return result;
		}
		#endregion

		#region constructor
		public LinearRamp(double size, Direction direction)
			: base(0)
		{
			GradientDirection = direction;

			Size = size;

			Pins[0] = -1;
			Pins[1] = 1;
		}
		#endregion
	}

	class OneMinus : ModuleBase
	{
		public OneMinus(ModuleBase input)
			: base(1)
		{
			m_modules[0] = input;
		}

		public override double GetValue(double x, double y, double z)
		{
			return 1 - m_modules[0].GetValue(x, y, z);
		}
	}

	#endregion
}
